之前讲了很多前端基础知识,这一篇换个口味,讲讲 Docker。然后再继续讲 JavaScript。
Docker 是一个开放源代码软件项目,让应用程序布署在软件容器下的工作可以自动化进行,借此在 Linux 操作系统上,提供一个额外的软件抽象层,以及操作系统层虚拟化的自动管理机制。Docker 利用 Linux 核心中的资源分脱机制,例如 cgroups,以及 Linux 核心名字空间(name space),来创建独立的软件容器(containers)。这可以在单一 Linux 实体下运作,避免启动一个虚拟机造成的额外负担。Linux 核心对名字空间的支持完全隔离了工作环境中应用程序的视野,包括进程树、网络、用户 ID 与挂载文件系统,而核心的 cgroup 提供资源隔离,包括 CPU、存储器、block I/O 与网络。从 0.9 版本起,Docker 在使用抽象虚拟是经由 libvirt 的 LXC 与 systemd - nspawn 提供界面的基础上,开始包括 libcontainer 库做为以自己的方式开始直接使用由 Linux 核心提供的虚拟化的设施。
软件开发最大的麻烦事之一,就是环境配置。 程序在本地开发后要放到线上,由于各种原因本地开发的机器可能要替换等等。那么开发环境一但改变,就要重新为程序安装各种服务与扩展。很多人想到,能不能从根本上解决问题,软件可以带环境安装?也就是说,安装的时候,把原始环境一模一样地复制过来。有两种解决方案:
- 虚拟机
- Linux 容器
最早我是 iOS 开发出身,使用 windows 平台安装 vmware 虚拟机进行开发,从中看出很多虚拟机的缺点:
- 资源占用多
- 启动慢
由于虚拟机存在这些缺点,Linux 发展出了另一种虚拟化技术: Linux 容器(Linux Containers,缩写为 LXC)。Linux 容器不是模拟一个完整的操作系统,而是对进程进行隔离。对于容器里面的进程来说,它接触到的各种资源都是虚拟的,从而实现与底层系统的隔离。由于容器是进程级别的,就没有虚拟机的那些缺点了。
Docker 概述
Docker 就是 Linux 容器的一种封装,提供简易的使用接口。最初是 dotCloud 公司创始人 Solomon Hykes 在法国期间发起的一个公司内部项目,于 2013 年 3 月以 Apache2.0 授权协议开源,主要项目代码在 GitHub 上进行维护。在 2013 年底,dotCloud 公司决定改名为 Docker。Docker 最初是在 Ubuntu 12.04 上开发实现的,Red Hat 则从 RHEL 6.5 开始对 Docker 进行支持,Google 也在其 PaaS 产品中广泛应用 Docker。Docker 现已成为目前最流行的 Linux 容器解决方案。
Docker 架构
Docker 使用客户端-服务器架构。Docker 客户端与守护进程交互,是操作容器的主要部件。Docker 客户端与守护进程可以运行在同一台机器上,你也可以通过客户端连接到远程的 Docker 守护进程。
Docker 守护进程管理 Docker 的对象,包括:
- images(镜像)
- containers(容器)
- networks(网络)
- volumes(数据卷)
Docker 的安装
Docker 是一个开源的商业产品,有两个版本: 社区版(Community Edition,缩写为 CE)和企业版(Enterprise Edition,缩写为 EE)。个人开发者使用 CE 版即可。
Docker CE 的安装请参考。
安装完成后,运行下面的命令,验证是否安装成功。
|
Docker 需要用户具有 sudo 权限,为了避免每次命令都输入 sudo,可以把用户加入 docker 用户组。
|
Docker 镜像
Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。
因为镜像包含操作系统完整的 root 文件系统,其体积往往是庞大的,因此在 Docker 设计时,就充分利用 Union FS 的技术,将其设计为分层存储的架构。
镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。
Docker 容器
镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的”类”和”实例”一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
前面讲过镜像使用的是分层存储,容器也是如此。每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为容器存储层。
容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。
按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用数据卷(Volume)或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。
数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷,容器删除或者重新运行之后,数据都不会丢失。
Docker Registry
镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。
一个 Docker Registry 中可以包含多个仓库(Repository),每个仓库可以包含多个标签(Tag),每个标签对应一个镜像。
通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。
最常使用的 Registry 公开服务是官方的 Docker Hub,这也是默认的 Registry,并拥有大量的高质量的官方镜像。国内从 Docker Hub 拉取镜像有时会遇到困难,此时可以配置镜像加速器。镜像加速器对于各个系统配置方式都不同,个人请参考不同系统进行配置。
国内也有一些云服务商提供类似于 Docker Hub 的公开服务。比如腾讯云镜像仓库、网易云镜像服务、DaoCloud 镜像市场、阿里云镜像库等。
除了使用公开服务外,用户还可以在本地搭建私有 Docker Registry。
Docker 基本操作
镜像
搜索镜像
|
获取镜像
|
如果不指定 tag,则默认拉取 latest。
查看本地镜像
|
创建本地镜像
使用 Dockerfile 文件,使用 docker build 命令进行构建:
|
- -t
为新镜像设置名称 - 通常在 Dockerfile 文件所在路径执行 docker build,这时可以使用 . 表示在当前目录
删除镜像
|
清理未使用镜像
|
迁移
|
容器
创建并运行本地容器
|
上面命令的说明:
- -t: 分配一个 pseudo-TTY
- -i: –interactive 参数缩写,表示交互模式,如果没有 attach 保持 STDIN 打开状态
- ubuntu: 运行的镜像名称,如果不指定 tag,默认为 latest 标签
- /bin/bash: 容器中运行的应用
对于 web 服务,我们还经常使用 -p 参数,指定宿主端口与容器端口的映射。
如何退出这个 bash?有两种方法,两种方法的效果完全不同:
- 直接 exit,这时候 bash 程序终止,容器进入到停止状态
- 使用组合键退出,仍然保持容器运行,我们可以随时回来到这个 bash 中来,组合键是 Ctrl-p + Ctrl-q,就可以退出到我们的宿主机了
查看本地容器
|
-a 参数指明查看所有容器,不论是否正在运行,如果没有 -a 参数,则只列出正在运行的容器。
停止容器
|
重新启动容器
|
删除容器
|
查看 Docker 容器或镜像的一些内部信息
|
exec
我们可以使用 exec 对容器执行一些操作:
|
–link 选项使得我们可以更好的进行容器间通信。上述操作是将 mynginx 映射为一个域名记录在 centos 的 /etc/hosts 文件中,在 centos 内部使用 mynginx 就可以找到 mynginx 容器对应的 ip,使得我们不必知道 mynginx 的具体 ip 就可以与之通信。
查看日志
|
-f 选型用来跟踪日志输出。
通过 Volumn 共享文件
Volumn 是独立于容器之外的持久化存储。
之前我们启动的一个容器,使用的是容器内默认的文件系统。那么,我们该如何让这个容器使用 Host 上我们指定目录中的内容呢?
具体怎么做呢?比如容器中的 Nginx 默认的 web 根目录是 /var/www/html,最简单的,我们把这个目录映射出来就好了。
- 在 host 中,创建一个 /tmp/web 目录,并在其中添加一个 demo.html 文件
- 执行下面的命令启动 docker:
|
- 我们打开浏览器,访问 http://localhost:8080/demo.html 就可以看到刚才的文件了
注意: 这里,我们使用了 -v host_dir:container_dir 进行了目录映射。映射之后,在容器中,之前 /var/www/html 目录指定的内容就无法访问了。现在 /var/www/html 访问的,是 host 上的 /tmp/web 目录。
我们还可以使用 docker volume 命令,让 docker 管理我们的数据卷:
|
-d 参数表示后台运行容器,-e 参数表示传递环境变量。
构建你自己的 Docker 镜像
按照之前的做法,如果想使用一个 Nginx 容器,每次我们都是启动一个 bash 容器,然后再手工安装 Nginx。现在,是时候做些改变了。这一节我们来看如何基于修改过的容器,定制新的 Docker 镜像。
我们首先执行 docker diff 命令:
|
Docker 用类似 git 的形式记录了容器中的每一个文件变化。并且,我们还可以像 git 中提交代码一样,去提交这些变化。
|
其中:
- -a 表示 Author,即提交者的姓名
- -m 表示 Message,即本次提交的注释
- 123d26dbe5df,这是容器 ID,它表示了我们要制作的镜像最终的状态
- 1ess/nginx:0.1.0,这是新镜像的名称以及版本号
重新执行 docker images,就能看到我们新创建的 nginx 镜像了。
问题
|
执行上面的命令,你就会发现,并不会和我们想象的一样启动 Nginx,然后进入容器内部的 sh。而是容器执行一下就退出了。这是因为当我们执行 nginx 命令的时候,会启动两类进程: 首先启动的是作为管理调度的 master process,它继续生成实际处理 HTTP 请求的 worker process。默认情况下,master process 是一个守护进程,它启动之后,就会断掉和自己的父进程之间的关联,于是 Docker 就跟踪不到了,进而容器也就会退出了。因此,解决的办法,就是让 Nginx 的 master process 不要以守护进程的方式启动,而是以普通模式启动就好了。为此,我们得修改下 Nginx 的配置文件。
- 用我们新创建的镜像,启动一个执行 Bash 的容器:
|
- 修改这个容器中 Nginx 的配置文件,关掉守护进程模式:
|
- 执行 exit 从容器中退出
再次提交 docker 差异,生成新镜像。
使用 Dockerfile 自动化镜像构建
除了像之前一样手工打造一个新镜像,Docker 还提供了脚本的功能,允许我们把打造镜像的过程”记录”在一个脚本里,并且自动”回放”出来。这样,无论是我们要部署一个新的环境,还是把自己的镜像分享给其他开发者,都很方便。
创建一个叫做 Dockerfile 的文件,这里要注意文件的名称和大小写。Dockerfile 是 docker 默认会使用的文件名。
在 Dockerfile 中,添加下面内容:
|
在上面的文件,所有大写字母,都是 Dockerfile 中的命令。其中:
- FROM 指的是构建新镜像的基础,也就是说,我们要基于 ubuntu:16.04 这个镜像定制自己的镜像
- LABEL 用于定义一些容器的 metadata,我们可能会在一些地方看到使用 MAINTAINER 命令设置维护者信息。不过 MAINTAINER 已经被 Docker 标记为过期了,因此,我们应该统一使用 LABEL 的这种形式
- RUN 用于设置构建新镜像的各种动作。实际上,我们一共执行了 4 个动作,分别是: 安装 Nginx、清理下载安装包、清除临时文件、关闭 Nginx 守护进程模式。但是,我们却使用了 && 把这 4 个动作写成了一个 RUN 命令,而没有使用不同的 RUN 命令分别执行这些动作。作为一个最佳实践,在构建一个新镜像时,我们应该尽可能减少 RUN 命令的使用次数,这样可以减少镜像的大小
- CMD 用于设置容器启动时默认执行的命令,显然,我们就是要启动 nginx
这样,这个简单的镜像构建脚本就完成了。
我们执行下面的命令构建镜像,并启动容器:
|
这里:
- 当我们执行 docker build 的时候,docker 就会默认在当前目录中,查找一个叫做 Dockerfile 的文件名作为构建脚本。或者我们也可以通过 -f filename 的形式指定成其他文件
- -t 用于设置新镜像的名称和 tag
- . 用于设置构建镜像时的上下文环境,这个环境不一定是当前目录。在 Dockerfile 中,所有的相对路径都会基于这个上下文环境指定的目录
这样新版本的 Nginx 镜像就构建完成了。
发布镜像文件
首先,去 hub.docker.com 注册一个账户。然后,用下面的命令登录:
|
接着,就可以使用如下命令发布镜像:
|